package com.lazerycode.jmeter.mojo;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.GLOBAL_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.JMETER_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.REPORT_GENERATOR_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.SAVE_SERVICE_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.SYSTEM_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.UPGRADE_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.USER_PROPERTIES;
import static com.lazerycode.jmeter.properties.ConfigurationFiles.values;
import static org.apache.commons.io.FileUtils.copyInputStreamToFile;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.filter.DependencyFilterUtils;
import com.lazerycode.jmeter.exceptions.DependencyResolutionException;
import com.lazerycode.jmeter.exceptions.IOException;
import com.lazerycode.jmeter.json.TestConfig;
import com.lazerycode.jmeter.properties.ConfigurationFiles;
import com.lazerycode.jmeter.properties.PropertiesFile;
import com.lazerycode.jmeter.properties.PropertiesMapping;
@Mojo(name = "configure", defaultPhase = LifecyclePhase.COMPILE)
public class ConfigureJMeterMojo extends AbstractJMeterMojo {
@Component
private RepositorySystem repositorySystem;
@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
private RepositorySystemSession repositorySystemSession;
@Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
private List<RemoteRepository> repositoryList;
/**
* Name of the base config json file
*/
private final String baseConfigFile = "/config.json";
/**
* The version of JMeter that this plugin will use to run tests.
* We use a hard coded list of artifacts to configure JMeter locally,
* if you change this version number the list of artifacts required to run JMeter may change.
* If this happens you will need to override the <jmeterArtifacts> element.
*/
@Parameter(defaultValue = "3.2")
private String jmeterVersion;
/**
* A list of artifacts that we use to configure JMeter.
* This list is hard coded by default, you can override this list and supply your own list of artifacts for JMeter.
* This would be useful if you want to use a different version of JMeter that has a different list of required artifacts.
* <p/>
* <jmeterExtensions>
* <artifact>kg.apc:jmeter-plugins:1.3.1</artifact>
* <jmeterExtensions>
*/
@Parameter
private List<String> jmeterArtifacts = new ArrayList<>();
/**
* A list of artifacts that the plugin should ignore.
* This would be useful if you don't want specific dependencies brought down by JMeter (or any used defined artifacts) copied into the JMeter directory structure.
* <p/>
* <ignoredArtifacts>
* <artifact>org.bouncycastle:bcprov-jdk15on:1.49</artifact>
* <ignoredArtifacts>
*/
@Parameter
private List<String> ignoredArtifacts = new ArrayList<>();
/**
* Download all dependencies of files you want to add to lib/ext and copy them to lib/ext too
* <p/>
* <downloadExtensionDependencies>
* <true>
* <downloadExtensionDependencies>
*/
@Parameter(defaultValue = "true")
protected boolean downloadExtensionDependencies;
/**
* A list of artifacts that should be copied into the lib/ext directory e.g.
* <p/>
* <jmeterExtensions>
* <artifact>kg.apc:jmeter-plugins:1.3.1</artifact>
* <jmeterExtensions>
*/
@Parameter
protected List<String> jmeterExtensions = new ArrayList<>();
/**
* Download all transitive dependencies of the JMeter artifacts.
* <p/>
* <downloadJMeterDependencies>
* <false>
* <downloadJMeterDependencies>
*/
@Parameter(defaultValue = "false")
protected boolean downloadJMeterDependencies;
/**
* Download all optional transitive dependencies of artifacts.
* <p/>
* <downloadOptionalDependencies>
* <true>
* <downloadOptionalDependencies>
*/
@Parameter(defaultValue = "false")
protected boolean downloadOptionalDependencies;
/**
* Download all dependencies of files you want to add to lib/junit and copy them to lib/junit too
* <p/>
* <downloadLibraryDependencies>
* <true>
* <downloadLibraryDependencies>
*/
@Parameter(defaultValue = "true")
protected boolean downloadLibraryDependencies;
/**
* A list of artifacts that should be copied into the lib/junit directory e.g.
* <p/>
* <junitLibraries>
* <artifact>com.lazerycode.junit:junit-test:1.0.0</artifact>
* <junitLibraries>
*/
@Parameter
protected List<String> junitLibraries = new ArrayList<>();
/**
* Absolute path to JMeter custom (test dependent) properties file.
*/
@Parameter
protected Map<String, String> propertiesJMeter = new HashMap<>();
/**
* JMeter Properties that are merged with precedence into default JMeter file in saveservice.properties
*/
@Parameter
protected Map<String, String> propertiesSaveService = new HashMap<>();
/**
* JMeter Properties that are merged with precedence into default JMeter file in reportgenerator.properties
*/
@Parameter
protected Map<String, String> propertiesReportGenerator = new HashMap<>();
/**
* JMeter Properties that are merged with precedence into default JMeter file in upgrade.properties
*/
@Parameter
protected Map<String, String> propertiesUpgrade = new HashMap<>();
/**
* JMeter Properties that are merged with precedence into default JMeter file in user.properties
* user.properties takes precedence over jmeter.properties
*/
@Parameter
protected Map<String, String> propertiesUser = new HashMap<>();
/**
* JMeter Global Properties that override those given in jmeterProps. <br>
* This sets local and remote properties (JMeter's definition of global properties is actually remote properties)
* and overrides any local/remote properties already set
*/
@Parameter
protected Map<String, String> propertiesGlobal = new HashMap<>();
/**
* (Java) System properties set for the test run.
* Properties are merged with precedence into default JMeter file system.properties
*/
@Parameter
protected Map<String, String> propertiesSystem = new HashMap<>();
/**
* Path under which .properties files are stored.
*/
@Parameter(defaultValue = "${basedir}/src/test/jmeter")
protected File propertiesFilesDirectory;
/**
* Replace the default JMeter properties with any custom properties files supplied.
* (If set to false any custom properties files will be merged with the default JMeter properties files, custom properties will overwrite default ones)
*/
@Parameter(defaultValue = "true")
protected boolean propertiesReplacedByCustomFiles;
// TODO move customPropertiesFiles here;
/**
* Set the format of the results generated by JMeter
* Valid values are: xml, csv (XML set by default).
*/
@Parameter(defaultValue = "xml")
protected String resultsFileFormat;
protected boolean resultsOutputIsCSVFormat = false;
public static final String JMETER_CONFIG_ARTIFACT_NAME = "ApacheJMeter_config";
private static final String JMETER_GROUP_ID = "org.apache.jmeter";
protected static Artifact jmeterConfigArtifact;
protected static File customPropertiesDirectory;
protected static File libDirectory;
protected static File libExtDirectory;
protected static File libJUnitDirectory;
/**
* Configure a local instance of JMeter
*
* @throws MojoExecutionException
* @throws MojoFailureException
*/
@Override
public void doExecute() throws MojoExecutionException, MojoFailureException {
getLog().info("-------------------------------------------------------");
getLog().info(" Configuring JMeter...");
getLog().info("-------------------------------------------------------");
generateJMeterDirectoryTree();
configureJMeterArtifacts();
populateJMeterDirectoryTree();
copyExplicitLibraries(jmeterExtensions, libExtDirectory, downloadExtensionDependencies);
copyExplicitLibraries(junitLibraries, libJUnitDirectory, downloadLibraryDependencies);
configurePropertiesFiles();
generateTestConfig();
}
/**
* Generate the directory tree utilised by JMeter.
*/
protected void generateJMeterDirectoryTree() {
workingDirectory = new File(jmeterDirectory, "bin");
workingDirectory.mkdirs(); //TODO remove this, it's covered in extractConfigSettings()
customPropertiesDirectory = new File(jmeterDirectory, "custom_properties");
customPropertiesDirectory.mkdirs();
libDirectory = new File(jmeterDirectory, "lib");
libExtDirectory = new File(libDirectory, "ext");
libExtDirectory.mkdirs();
libJUnitDirectory = new File(libDirectory, "junit");
libJUnitDirectory.mkdirs();
testFilesBuildDirectory.mkdirs();
resultsDirectory.mkdirs();
if(generateReports) {
reportDirectory.mkdirs();
}
logsDirectory.mkdirs();
}
protected void configurePropertiesFiles() throws MojoExecutionException, MojoFailureException {
propertiesMap.put(JMETER_PROPERTIES, new PropertiesMapping(propertiesJMeter));
propertiesMap.put(SAVE_SERVICE_PROPERTIES, new PropertiesMapping(propertiesSaveService));
propertiesMap.put(UPGRADE_PROPERTIES, new PropertiesMapping(propertiesUpgrade));
propertiesMap.put(SYSTEM_PROPERTIES, new PropertiesMapping(propertiesSystem));
propertiesMap.put(REPORT_GENERATOR_PROPERTIES, new PropertiesMapping(propertiesReportGenerator));
propertiesMap.put(USER_PROPERTIES, new PropertiesMapping(propertiesUser));
propertiesMap.put(GLOBAL_PROPERTIES, new PropertiesMapping(propertiesGlobal));
setJMeterResultFileFormat();
for (ConfigurationFiles configurationFile : values()) {
File suppliedPropertiesFile = new File(propertiesFilesDirectory, configurationFile.getFilename());
File propertiesFileToWrite = new File(workingDirectory, configurationFile.getFilename());
PropertiesFile somePropertiesFile = new PropertiesFile(jmeterConfigArtifact, configurationFile);
somePropertiesFile.loadProvidedPropertiesIfAvailable(suppliedPropertiesFile, propertiesReplacedByCustomFiles);
somePropertiesFile.addAndOverwriteProperties(propertiesMap.get(configurationFile).getAdditionalProperties());
somePropertiesFile.writePropertiesToFile(propertiesFileToWrite);
propertiesMap.get(configurationFile).setPropertiesFile(somePropertiesFile);
}
for (File customPropertiesFile : customPropertiesFiles) {
PropertiesFile customProperties = new PropertiesFile(customPropertiesFile);
String customPropertiesFilename = FilenameUtils.getBaseName(customPropertiesFile.getName()) + "-" + UUID.randomUUID().toString() + FilenameUtils.getExtension(customPropertiesFile.getName());
customProperties.writePropertiesToFile(new File(customPropertiesDirectory, customPropertiesFilename));
}
setDefaultPluginProperties(workingDirectory.getAbsolutePath());
}
protected void generateTestConfig() throws MojoExecutionException {
try (InputStream configFile = this.getClass().getResourceAsStream(baseConfigFile)) {
TestConfig testConfig = new TestConfig(configFile);
testConfig.setResultsOutputIsCSVFormat(resultsOutputIsCSVFormat);
testConfig.writeResultFilesConfigTo(testConfigFile);
} catch(java.io.IOException ex) {
throw new MojoExecutionException("Exception creating TestConfig", ex);
}
}
protected void setJMeterResultFileFormat() {
if (generateReports || resultsFileFormat.toLowerCase().equals("csv")) {
propertiesJMeter.put("jmeter.save.saveservice.output_format", "csv");
resultsOutputIsCSVFormat = true;
} else {
propertiesJMeter.put("jmeter.save.saveservice.output_format", "xml");
resultsOutputIsCSVFormat = false;
}
}
public void setDefaultPluginProperties(String userDirectory) {
//JMeter uses the system property "user.dir" to set its base working directory
System.setProperty("user.dir", userDirectory);
//Prevent JMeter from throwing some System.exit() calls
System.setProperty("jmeterengine.remote.system.exit", "false");
System.setProperty("jmeterengine.stopfail.system.exit", "false");
}
/**
* This sets the default list of artifacts that we use to set up a local instance of JMeter.
* We only use this default list if <jmeterArtifacts> has not been overridden in the POM.
*/
private void configureJMeterArtifacts() {
if (jmeterArtifacts.isEmpty()) {
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_components:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_config:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_core:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_ftp:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_functions:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_http:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_java:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_jdbc:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_jms:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_junit:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_ldap:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_mail:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_mongodb:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_native:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":ApacheJMeter_tcp:" + jmeterVersion);
jmeterArtifacts.add(JMETER_GROUP_ID + ":jorphan:" + jmeterVersion); //TODO move to lib dir
}
}
private void populateJMeterDirectoryTree() throws DependencyResolutionException, IOException {
if (jmeterArtifacts.isEmpty()) {
throw new DependencyResolutionException("No JMeter dependencies specified!, check jmeterArtifacts and jmeterVersion elements");
}
for (String desiredArtifact : jmeterArtifacts) {
Artifact returnedArtifact = getArtifactResult(new DefaultArtifact(desiredArtifact));
switch (returnedArtifact.getArtifactId()) {
case JMETER_CONFIG_ARTIFACT_NAME:
jmeterConfigArtifact = returnedArtifact;
//TODO Could move the below elsewhere if required.
extractConfigSettings(jmeterConfigArtifact);
break;
case "ApacheJMeter":
runtimeJarName = returnedArtifact.getFile().getName();
copyArtifact(returnedArtifact, workingDirectory);
copyTransitiveRuntimeDependenciesToLibDirectory(returnedArtifact, downloadJMeterDependencies);
break;
default:
copyArtifact(returnedArtifact, libExtDirectory);
copyTransitiveRuntimeDependenciesToLibDirectory(returnedArtifact, downloadJMeterDependencies);
}
}
if (confFilesDirectory.exists()) {
CopyFilesInTestDirectory(confFilesDirectory, new File(jmeterDirectory, "bin"));
}
}
/**
* Copy a list of libraries to a specific folder.
*
* @param desiredArtifacts A list of artifacts
* @param destination A destination folder to copy these artifacts to
* @throws DependencyResolutionException
* @throws IOException
*/
private void copyExplicitLibraries(List<String> desiredArtifacts, File destination, boolean downloadDependencies) throws DependencyResolutionException, IOException {
for (String desiredArtifact : desiredArtifacts) {
Artifact returnedArtifact = getArtifactResult(new DefaultArtifact(desiredArtifact));
copyArtifact(returnedArtifact, destination);
if (downloadDependencies) {
copyTransitiveRuntimeDependenciesToLibDirectory(returnedArtifact, true);
}
}
}
/**
* Find a specific artifact in a remote repository
*
* @param desiredArtifact The artifact that we want to find
* @return Will return an ArtifactResult object
* @throws DependencyResolutionException
*/
private Artifact getArtifactResult(Artifact desiredArtifact) throws DependencyResolutionException {
ArtifactRequest artifactRequest = new ArtifactRequest();
artifactRequest.setArtifact(desiredArtifact);
artifactRequest.setRepositories(repositoryList);
try {
return repositorySystem.resolveArtifact(repositorySystemSession, artifactRequest).getArtifact();
} catch (ArtifactResolutionException e) {
throw new DependencyResolutionException(e.getMessage(), e);
}
}
/**
* Collate a list of transitive runtime dependencies that need to be copied to the /lib directory and then copy them there.
*
* @param artifact The artifact that is a transitive dependency
* @throws DependencyResolutionException
* @throws IOException
*/
private void copyTransitiveRuntimeDependenciesToLibDirectory(Artifact artifact, boolean getDependenciesOfDependency) throws DependencyResolutionException, IOException {
CollectRequest collectRequest = new CollectRequest();
collectRequest.setRoot(new Dependency(artifact, JavaScopes.RUNTIME));
collectRequest.setRepositories(repositoryList);
DependencyFilter dependencyFilter = DependencyFilterUtils.classpathFilter();
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, dependencyFilter);
try {
List<DependencyNode> artifactDependencyNodes = repositorySystem.resolveDependencies(repositorySystemSession, dependencyRequest).getRoot().getChildren();
for (DependencyNode dependencyNode : artifactDependencyNodes) {
if (getLog().isDebugEnabled()) {
getLog().debug("Dependency name: " + dependencyNode.toString());
getLog().debug("Dependency request trace: " + dependencyRequest.getCollectRequest().getTrace().toString());
getLog().debug("-------------------------------------------------------");
}
if (downloadOptionalDependencies || !dependencyNode.getDependency().isOptional()) {
Artifact returnedArtifact = getArtifactResult(dependencyNode.getArtifact());
if (!returnedArtifact.getArtifactId().startsWith("ApacheJMeter_")) {
copyArtifact(returnedArtifact, libDirectory);
}
if (getDependenciesOfDependency) {
copyTransitiveRuntimeDependenciesToLibDirectory(returnedArtifact, true);
}
}
}
} catch (org.eclipse.aether.resolution.DependencyResolutionException e) {
throw new DependencyResolutionException(e.getMessage(), e);
}
}
/**
* Copy an Artifact to a directory
*
* @param artifact Artifact that needs to be copied.
* @param destinationDirectory Directory to copy the artifact to.
* @throws IOException Unable to copy file
* @throws DependencyResolutionException Unable to resolve dependency
*/
private void copyArtifact(Artifact artifact, File destinationDirectory) throws IOException, DependencyResolutionException {
for (String ignoredArtifact : ignoredArtifacts) {
Artifact artifactToIgnore = getArtifactResult(new DefaultArtifact(ignoredArtifact));
if (artifact.getFile().getName().equals(artifactToIgnore.getFile().getName())) {
getLog().debug(artifact.getFile().getName() + " has not been copied over because it is in the ignore list.");
return;
}
}
try {
File artifactToCopy = new File(destinationDirectory + File.separator + artifact.getFile().getName());
getLog().debug("Checking: " + artifactToCopy.getAbsolutePath() + "...");
if (!artifactToCopy.exists()) {
getLog().debug("Copying: " + artifactToCopy.getAbsolutePath() + "...");
FileUtils.copyFileToDirectory(artifact.getFile(), destinationDirectory);
}
} catch (java.io.IOException e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* Extract the configuration settings (not properties files) from the configuration artifact and load them into the /bin directory
*
* @param artifact Configuration artifact
* @throws IOException
*/
private void extractConfigSettings(Artifact artifact) throws IOException {
try (JarFile configSettings = new JarFile(artifact.getFile())) {
Enumeration<JarEntry> entries = configSettings.entries();
while (entries.hasMoreElements()) {
JarEntry jarFileEntry = entries.nextElement();
// Only interested in files in the /bin directory that are not properties files
if (!jarFileEntry.isDirectory() && jarFileEntry.getName().startsWith("bin") && !jarFileEntry.getName().endsWith(".properties")) {
File fileToCreate = new File(jmeterDirectory, jarFileEntry.getName());
copyInputStreamToFile(configSettings.getInputStream(jarFileEntry), fileToCreate);
} else if (!jarFileEntry.isDirectory() && jarFileEntry.getName().startsWith("bin/report-template")) {
File fileToCreate = new File(jmeterDirectory, jarFileEntry.getName());
copyInputStreamToFile(configSettings.getInputStream(jarFileEntry), fileToCreate);
}
}
} catch (java.io.IOException e) {
throw new IOException(e.getMessage(), e);
}
}
}